#!/usr/bin/env python
#
# Copyright 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import os
import sys
import wx
import wx.lib.buttons
from launcher import project
import launcher
from wxgladegen import main_frame


class MainFrame(main_frame.GenMainFrame):
  """The main view "window" (Frame in wx-speak) for this application.

  Contains a list of projects, a toolbar, etc.
  """

  # Column labels for our table.
  COL_LABELS = ('runstate', 'name', 'path', 'port')

  # Mapping of icon file names to their corresponding project run state.
  ICON_STATE_MAP = {
      'off.png': project.Project.STATE_STOP,
      'on.png': project.Project.STATE_RUN,
      'productionon.png': project.Project.STATE_PRODUCTION_RUN,
      'starting.png': project.Project.STATE_STARTING,
      'died.png': project.Project.STATE_DIED,
  }

  # Icon file names.
  ICON_FILE_NAMES = ICON_STATE_MAP.keys()

  # MenuID of our "Demos" menu, as encoded in MainFrame.wxg
  ID_DEMOS_MENU = 6000

  # Size of the +/- buttons in the status bar.
  STATUS_BAR_BUTTON_SIZE = 24

  # Minimum size of the window.
  WINDOW_MIN_SIZE = (500, 200)

  def __init__(self, parent, id, table, preferences, app_controller,
               task_controller, *args, **kwds):
    """Create a new MainFrame, based on GenMainFrame generated by wxglade.

    Args:
      parent: parent window (standard wxPython parameter)
      id: object id (standard wxPython parameter)
      table: MainTable of data for our matrix of projects. (M in MVC)
      preferences: a launcher.Preferences (M in MVC)
      app_controller: the main application controller (C in MVC)
      task_controller: a task-related controller
    """
    main_frame.GenMainFrame.__init__(self, parent, id)

    # MVC items.
    self._table = table
    self._preferences = preferences
    self._task_controller = task_controller
    self._app_controller = app_controller

    # Housekeeping
    self._icon_index_state_map = {}  # Maps project states to image array index.
    self._status_bar_buttons = []

    self._LoadImages()
    self._RestoreWindowPosition()
    self._BuildDemoMenu()
    self._SetupStatusBar()
    self._AdjustEnabledStatesBasedOnSelection()

    # Bind resizing so we can remember the last mainframe window position.
    # Glade doesn't have a way to specify this kind of stuff for arbitrary
    # classes.
    self.Bind(wx.EVT_CLOSE, self.OnClose, self)

    self.SetMinSize(self.WINDOW_MIN_SIZE)
    self._SetIcon()

  def _RestoreWindowPosition(self):
    """Restore the main window position from the preferences.

    Reads the last saved window position from the preferences (if it exists).
    Also attempts to keep the window visible if it was previously on another
    monitor that might be disconnected.
    """
    if not self._preferences:
      return

    prefname = launcher.Preferences.PREF_MAIN_WINDOW_RECT
    pref_rect = self._preferences.Get(prefname)
    if pref_rect:
      rect = [int(x) for x in pref_rect.split(' ')]
      self.SetSize(rect[2:4])

      # Make sure this rectangle is visible on any of the attached displays.
      # If not, don't restore the position, but use the default origin.
      for i in range(wx.Display.GetCount()):
        displayRect = wx.Display(i).GetGeometry()
        if displayRect.Intersects(rect):
          self.SetPosition(rect[0:2])

  def _LoadImages(self):
    """Load images we will use for our display."""
    self._imagelist = wx.ImageList(16, 16, True)
    # Images are added to wx ListCtrls by specifying an index
    # into an image list.  Projects have a runstate attribute, with different
    # states corresponding to different icons.  Load the images and
    # build a mapping table from project states to their index in the
    # image list, so the actual ordering of the names in ICON_FILE_NAMES
    # is unimportant
    for index, icon_file_name in enumerate(self.ICON_FILE_NAMES):
      # Load the image.
      image = wx.Bitmap(os.path.join('images', icon_file_name))
      # Add it to the image list.  This icon lives at index in the list.
      self._imagelist.Add(image)
      # Locate the launcher.Project.runstate for this icon.
      state = self.ICON_STATE_MAP[icon_file_name]
      # Finally add the STATE_WHATEVER -> index map.
      self._icon_index_state_map[state] = index
    self._listctrl.AssignImageList(self._imagelist, wx.IMAGE_LIST_SMALL)

  def _BuildDemoMenu(self, demo_dir=None):
    """Build the demos menu, using projects in the given demo directory.

    Args:
      demo_dir: the directory that contains demos.  If None (e.g. if
        not unit testing), use a default.
    """
    # Build the demos menu.  Create an item for each demo.
    demo_menu = wx.Menu()
    sdk_directory = launcher.Platform().AppEngineBaseDirectory()
    if not demo_dir and not sdk_directory:
      return
    demo_directory = demo_dir or os.path.join(sdk_directory, 'demos')

    for demo in os.listdir(demo_directory):
      full_demo_path = os.path.join(demo_directory, demo)
      if os.path.isdir(full_demo_path):
        item = wx.MenuItem(demo_menu, -1, demo)
        demo_menu.AppendItem(item)
        self.Bind(wx.EVT_MENU, self._CreateDemoByNameFunction(full_demo_path),
                  item)

    # Find the old demo item and replace it.  This is a little
    # awkward, but it appears to be the consequence of mixing
    # wxGlade generated menus with manually created ones.
    if not demo_menu.GetMenuItemCount():
      return
    old_demo_item = self.GetMenuBar().FindItemById(self.ID_DEMOS_MENU)
    if not old_demo_item:
      # We get here in a unit test -- menubar not realized
      return
    menu = old_demo_item.GetMenu()
    for pos in range(menu.GetMenuItemCount()):
      if old_demo_item.Id == menu.FindItemByPosition(pos).Id:
        break
    menu.InsertMenu(pos, -1, 'Demos', demo_menu)
    menu.DeleteItem(old_demo_item)

  def _CreateDemoByNameFunction(self, path):
    """Create and return a DemoByName function.

    The returned function takes one arg (an event) so is suitable for
    a Bind() callback.  This curries the value for path for a real
    call to our _app_controller where we can specify path explicitly.

    Args:
      path: a full path to curry to a InstallDemoByName() call
    Returns:
      A function that takes one arg, an event, but calls
      InstallDemoByName() with the input path.
    """
    def InstallDemoByName(event):
      self._app_controller.InstallDemoByName(path)
    return InstallDemoByName

  def _AddStatusBarButton(self, button_id, label, callback):
    """Add a new button to the status bar.

    Args:
      id: numerical ID to identify the new button.
      label: The text label for the button.
      callback: The functionx to invoke when the button is pressed.

    In addition to adding the button to the status bar, this appends the new
    button to self._status_bar_buttons.
    """
    button = wx.lib.buttons.GenButton(self._statusbar, button_id, label)
    button.SetUseFocusIndicator(False)
    self.Bind(wx.EVT_BUTTON, callback, button)
    self._status_bar_buttons.append(button)

  def _SetupStatusBar(self):
    """Create the window's status bar and populate it with the +/- buttons."""
    # Creating the status bar in the glade-generated superclass code makes
    # embedded objects (like buttons) ignore mouse clicks on the Mac.
    self._statusbar = self.CreateStatusBar(2, 0)
    self.SetStatusWidths([self.STATUS_BAR_BUTTON_SIZE] * 2)
    self._AddStatusBarButton(main_frame.PLUS_BUTTON, '+', self.OnAddNewApp)
    self._AddStatusBarButton(main_frame.MINUS_BUTTON, '-', self.OnRemoveApp)
    self._OnStatusbarSize(None)

  def _OnStatusbarSize(self, event):
    """ Adjust the +/- buttons' position when the status bar changes size."""
    for index, button in enumerate(self._status_bar_buttons):
      rect = self._statusbar.GetFieldRect(index)
      button.SetPosition((rect.x, rect.y))
      button.SetSize((rect.width, rect.height))
    if (event):
      event.Skip()

  def UnselectAll(self):
    """Empty out the listctrl's selection"""
    for index in range(self._listctrl.GetItemCount()):
      self._listctrl.Select(index, 0)

  def RefreshView(self, projects):
    listCtrl = self._listctrl
    # Hang on to the currently selected projects, and attempt to
    # restore them later.
    selectedProjects = self.SelectedProjects()

    listCtrl.ClearAll()

    labels = self.COL_LABELS
    listCtrl.InsertColumn(0, '', format=wx.LIST_FORMAT_CENTRE, width=22)
    listCtrl.InsertColumn(1, labels[1],
                          format=wx.LIST_FORMAT_LEFT, width=100)
    listCtrl.InsertColumn(2, labels[2],
                          format=wx.LIST_FORMAT_LEFT, width=400)
    listCtrl.InsertColumn(3, labels[3],
                          format=wx.LIST_FORMAT_LEFT)

    for row, project in enumerate(projects):
      # Map the project's runstate to its corresponding imagelist
      # index before adding it to the ListCtrl.
      index = self._icon_index_state_map[project.runstate]
      listCtrl.InsertImageItem(sys.maxint, index)

      # If there is a problem with a project (e.g. it has been deleted
      # out from under us), display it in red.
      self._MarkRowValidity(listCtrl, row, project.valid)

      listCtrl.SetStringItem(row, 1, project.name)
      listCtrl.SetStringItem(row, 2, project.path)
      listCtrl.SetStringItem(row, 3, str(project.port))

    self.SetSelectedProjects(selectedProjects)

    # Make sure that a refresh caused from switching out and back
    # doesn't resize the list contents to the min size.
    listCtrl.resizeLastColumn(0)

  def _MarkRowValidity(self, listCtrl, row, valid):
    """Visually mark the valid state of a row.

    Args:
      listCtrl: The listCtrl to modify.
      row: The row to mark.
      valid: A bool to specify validity.
    """
    listCtrl.SetItemTextColour(row, ['RED', 'BLACK'][valid])

  def _AdjustEnabledStatesBasedOnSelection(self):
    """Enable and disable controls based on the contents of the selection."""
    helper = launcher.MainframeSelectionHelper()
    helper.AdjustMainFrame(self, self.SelectedProjects())

  def _SetIcon(self):
    """Tell wx about our Windows icon (if relevant), and use it."""
    try:
      import win32api
    except ImportError:
      return
    exe_name = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
    self.SetIcon(wx.Icon(exe_name, wx.BITMAP_TYPE_ICO))

  def GetButtonByID(self, button_id):
    """Map a button id to the corresponding wx.Button.

    Mainly used by the MainframeSelectionHelper to get ahold of the
    minus button, which is disabled if there is no selection.
    """
    return {
      main_frame.PLUS_BUTTON: self._status_bar_buttons[0],
      main_frame.MINUS_BUTTON: self._status_bar_buttons[1],
    }.get(button_id)

  def CloseWindow(self, event):
    self.OnExit(event)

  def OnOpenSDK(self, event):
    self._task_controller.OpenSDK(event)

  def OnExit(self, event):
    # PySimpleApp, used in unit tests, don't have an OnExit().
    app = wx.GetApp()
    if hasattr(app, 'OnExit'):
      app.OnExit()

  def OnRun(self, event):
    self._task_controller.Run(event)

  def OnRunStrict(self, event):
    self._task_controller.RunStrict(event)

  def OnStop(self, event):
    self._task_controller.Stop(event)

  def OnBrowse(self, event):
    self._task_controller.Browse(event)

  def OnLogs(self, event):
    self._task_controller.Logs(event)

  def OnSdkConsole(self, event):
    self._task_controller.SdkConsole(event)

  def OnEdit(self, event):
    self._task_controller.Edit(event)

  def OnAppOpen(self, event):
    """Open in the native file navigator (Finder, Explorer, etc)"""
    self._task_controller.Open(event)

  def OnDeploy(self, event):
    self._task_controller.Deploy(event)

  def OnDashboard(self, event):
    self._task_controller.Dashboard(event)

  def SelectedProjects(self):
    """Return a list of currently selected projects."""

    return [self._table.ProjectAtIndex(index)
            for index in range(self._listctrl.GetItemCount())
            if self._listctrl.IsSelected(index)]

  def SetSelectedProjects(self, selected_projects):
    """Attempt to select the given projects.

    Args:
      selectedProjects: a list of projects to attempt to select in the
        table.  Any projects that have disappeared since the
        selectedProjects list was created will, naturally, not be
        selected, but won't cause any problems.
    """
    for index in range(self._listctrl.GetItemCount()):
      if self._table.ProjectAtIndex(index) in selected_projects:
        self._listctrl.Select(index)

  def OnPreferences(self, event):
    self._app_controller.OnPreferences(event)

  def OnAbout(self, event):
    self._app_controller.OnAbout(event)

  def OnAddApp(self, event):
    self._app_controller.Add(event)

  def OnAddNewApp(self, event):
    self._app_controller.AddNew(event)

  def OnRemoveApp(self, event):
    self._app_controller.Remove(event)

  def OnAppSettings(self, event):
    self._app_controller.Settings(event)

  def OnHelp(self, event):
    self._app_controller.Help(event)

  def OnAppEngineHelp(self, event):
    self._app_controller.AppEngineHelp(event)

  def OnDemos(self, event):
    self._app_controller.Demos(event)

  def OnCheckForUpdates(self, event):
    self._app_controller.CheckForUpdates(event)

  def OnClose(self, event):
    if not self._preferences:
      return
    # Save the window's current position for later restoration.
    rect = ' '.join(map(str, self.GetRect().Get()))
    self._preferences.Set(launcher.Preferences.PREF_MAIN_WINDOW_RECT, rect)
    self._preferences.Save()
    # Now die -- no more windows!
    self.OnExit(event)

  def OnSelectionChange(self, event):
    self._AdjustEnabledStatesBasedOnSelection()
    event.Skip()

  def _GetTextFromClipboard(self):
    """Get the text from the clipboard.  Returns None if there isn't any."""
    text = None
    clipboard = wx.Clipboard()
    if clipboard and clipboard.Open():
      dataobject = wx.TextDataObject()
      success = clipboard.GetData(dataobject)
      if success:
        text = dataobject.GetText()
      clipboard.Close()
    return text

  def OnPaste(self, event):
    """Pasting a valid path triggers the 'add existing project' logic."""
    path = self._GetTextFromClipboard()
    if path and os.path.isdir(path):
      self._app_controller.Add(event, path)
